Die Boost C++ Bibliotheken


Kapitel 14: Datenstrukturen


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


14.1 Allgemeines

Es existieren zahlreiche Boost C++ Bibiotheken, auf die nicht die Definition von Containern zutrifft und die deswegen nicht im Kapitel 13, Container vorgestellt werden. Sie definieren andere Datenstrukturen, die Sie in diesem Kapitel kennenlernen werden. Zu diesen Datenstrukturen zählt zum Beispiel eine Klasse boost::tuple, die den aus dem C++ Standard bekannten Datentyp std::pair so erweitert, dass nicht mehr nur zwei, sondern beliebig viele Daten gespeichert werden können.

Neben der Klasse boost::tuple lernen Sie in diesem Kapitel die Klassen boost::any und boost::variant kennen, die es ermöglichen, Werte unterschiedlicher Datentypen in einer Variablen zu speichern. Variablen vom Typ boost::any verhalten sich wie Variablen in typenlosen Programmiersprachen, die jede beliebige Information speichern können. Variablen vom Typ boost::variant wiederum verhalten sich wie union-Variablen und können Daten bestimmter vorher festgelegter Typen speichern.


14.2 Boost.Tuple

Die Bibliothek Boost.Tuple stellt eine Klasse boost::tuple zur Verfügung, die als eine verallgemeinerte Version von std::pair bezeichnet werden kann. Während std::pair genau zwei Werte speichern kann, besteht bei boost::tuple die freie Wahl.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string, std::string> person; 
  person p("Boris", "Schaeling"); 
  std::cout << p << std::endl; 
} 

Um die Klasse boost::tuple verwenden zu können, muss die Headerdatei boost/tuple/tuple.hpp eingebunden werden. Möchten Sie Variablen vom Typ boost::tuple von Streams lesen und in Streams ausgeben, müssen Sie außerdem die Headerdatei boost/tuple/tuple_io.hpp einbinden. Boost.Tuple stellt keine Headerdatei zur Verfügung, die automatisch alle anderen Headerdateien einbindet.

boost::tuple wird grundsätzlich genauso verwendet wie std::pair. So können Sie wie im obigen Beispiel zwei Template-Parameter angeben, um zwei Werte - in diesem Fall beide vom Typ std::string - zu speichern.

Während die obige Typdefinition von person auch mit std::pair hätte erfolgen können, ist es möglich, Objekte vom Typ boost::tuple in einen Stream zu schreiben. Dazu muss wie bereits erwähnt die Headerdatei boost/tuple/tuple_io.hpp eingebunden werden, in der sich die Definitionen der entsprechenden Operatoren für boost::tuple befinden. So gibt obiges Programm (Boris Schaeling) aus.

Der entscheidende Unterschied zwischen boost::tuple und std::pair ist aber der, dass ein Tuple beliebig viele Werte speichern kann.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string, std::string, int> person; 
  person p("Boris", "Schaeling", 43); 
  std::cout << p << std::endl; 
} 

Obiges Programm speichert zusätzlich zum Vor- und Nachnamen die Schuhgröße einer Person. Alle drei Werte können in einem Tuple untergebracht werden. Wenn Sie das Programm ausführen, wird (Boris Schaeling 43) ausgegeben.

So wie es zu std::pair eine Hilfsfunktion std::make_pair() gibt, kann ein Tuple auch mit Hilfe einer Funktion boost::make_tuple() konstruiert werden.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <iostream> 

int main() 
{ 
  std::cout << boost::make_tuple("Boris", "Schaeling", 43) << std::endl; 
} 

Es ist auch möglich, ein Tuple zu erstellen, das Referenzen enthält. Sehen Sie sich dazu folgendes Beispiel an.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  std::string s = "Boris"; 
  std::cout << boost::make_tuple(boost::ref(s), "Schaeling", 43) << std::endl; 
} 

Während die Werte "Schaeling" und 43 als Kopie übergeben und direkt im Tuple gespeichert werden, ist das erste Element eine Referenz auf den String s. Um eine derartige Referenz zu erstellen, wird auf boost::ref() aus Boost.Ref zugegriffen. Entsprechend muss auf boost::cref() zugegriffen werden, wenn eine konstante Referenz erstellt werden soll.

Nachdem Sie gesehen haben, wie Tuple erstellt werden, wird im Folgenden auf Elemente eines Tuples zugegriffen. Bei std::pair geschieht dies über die Eigenschaften first und second. Da ein Tuple jedoch keine festgelegte Anzahl an Elementen besitzt, findet der Zugriff auf eine andere Art und Weise statt.

#include <boost/tuple/tuple.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string, std::string, int> person; 
  person p = boost::make_tuple("Boris", "Schaeling", 43); 
  std::cout << p.get<0>() << std::endl; 
  std::cout << boost::get<0>(p) << std::endl; 
} 

Es gibt zwei Möglichkeiten, auf ein Element in einem Tuple zuzugreifen: Entweder wird für das Tuple die Methode get() aufgerufen oder das Tuple als einziger Parameter an eine freistehende Funktion boost::get() übergeben. In beiden Fällen muss der Index des entsprechenden Elements im Tuple als Template-Parameter übergeben werden. Für obiges Beispiel bedeutet das, dass in beiden Fällen auf das erste Element im Tuple p zugegriffen wird und zweimal Boris ausgegeben wird.

Sollten Sie einen ungültigen Index angeben, kompiliert der Code nicht. Die Gültigkeit der Indizes wird zur Kompilierung überprüft, so dass es nicht zu Laufzeitfehlern kommen kann.

Um einen Wert in einem Tuple zu ändern, müssen Sie ebenfalls entweder auf die Methode get() oder die freistehende Funktion boost::get() zugreifen.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string, std::string, int> person; 
  person p = boost::make_tuple("Boris", "Schaeling", 43); 
  p.get<1>() = "Becker"; 
  std::cout << p << std::endl; 
} 

Sowohl get() als auch boost::get() geben eine Referenz zurück. Im obigen Beispiel wird entsprechend der Nachname geändert und vom Programm (Boris Becker 43) ausgegeben.

Boost.Tuple stellt nicht nur überladene Operatoren zur Verfügung, um Tuple auf einen Stream auszugeben oder von einem Stream zu lesen. Boost.Tuple definiert auch Vergleichsoperatoren. Wollen Sie Tuple vergleichen, müssen Sie eine zusätzliche Headerdatei einbinden, nämlich boost/tuple/tuple_comparison.hpp.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_comparison.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string, std::string, int> person; 
  person p1 = boost::make_tuple("Boris", "Schaeling", 43); 
  person p2 = boost::make_tuple("Boris", "Becker", 43); 
  std::cout << (p1 != p2) << std::endl; 
} 

Obiges Beispielprogramm gibt 1 aus, da die beiden Tuple p1 und p2 verschieden sind.

Die Headerdatei boost/tuple/tuple_comparison.hpp enthält auch Definitionen anderer Vergleichsoperatoren wie Größer-als, die einen lexikographischen Vergleich durchführen.

Boost.Tuple unterstützt eine besondere Form von Tuple namens Tier. Ein Tier ist ein Tuple, dessen Elemente alle Referenzen sind. Es kann mit der Funktion boost::tie() erstellt werden.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string&, std::string&, int&> person; 

  std::string firstname = "Boris"; 
  std::string surname = "Schaeling"; 
  int shoesize = 43; 
  person p = boost::tie(firstname, surname, shoesize); 
  surname = "Becker"; 
  std::cout << p << std::endl; 
} 

Im obigen Beispiel wird ein Tier p erstellt, das aus Referenzen auf die Variablen firstname, surname und shoesize besteht. Indem die Variable surname auf einen neuen Wert gesetzt wird, wird gleichzeitig das Tier geändert.

Anstatt boost::tie() zu verwenden hätte obiges Beispiel auch mit boost::make_tuple() und boost::ref() geschrieben werden können.

#include <boost/tuple/tuple.hpp> 
#include <boost/tuple/tuple_io.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  typedef boost::tuple<std::string&, std::string&, int&> person; 

  std::string firstname = "Boris"; 
  std::string surname = "Schaeling"; 
  int shoesize = 43; 
  person p = boost::make_tuple(boost::ref(firstname), boost::ref(surname), boost::ref(shoesize)); 
  surname = "Becker"; 
  std::cout << p << std::endl; 
} 

boost::tie() verkürzt die Schreibweise. Diese Funktion bietet sich auch an, wenn ein Tuple entpackt werden soll. Sehen Sie sich dazu folgendes Beispiel an, in dem eine Funktion ein Tuple zurückgibt und die einzelnen Werte im Tuple sofort in Variablen gespeichert werden.

#include <boost/tuple/tuple.hpp> 
#include <string> 
#include <iostream> 

boost::tuple<std::string, int> func() 
{ 
  return boost::make_tuple("Error message", 2009); 
}

int main() 
{ 
  std::string errmsg; 
  int errcode; 

  boost::tie(errmsg, errcode) = func(); 
  std::cout << errmsg << ": " << errcode << std::endl; 
} 

Mit Hilfe von boost::tie() werden der String "Error message" und die Fehlernummer 2009, die beide zusammen in einem Tuple von func() zurückgegeben werden, direkt in den Variablen errmsg und errcode gespeichert.


14.3 Boost.Any

Strengtypisierte Sprachen wie C++ verlangen, dass jede Variable einen ganz bestimmten Datentyp hat. Dieser Datentyp entscheidet darüber, welche Art von Information in einer Variablen gespeichert werden kann. Wer in einer Variablen eine beliebige Art von Information speichern möchte, muss eine Programmiersprache wie beispielsweise Javascript einsetzen: Dort kann in einer Variablen eine Zeichenkette, danach eine Zahl und dann ein Wahrheitswert gespeichert werden. In Javascript kann jede Variable jede beliebige Information speichern.

Die Bibliothek Boost.Any stellt eine Klasse boost::any zur Verfügung, die es ähnlich wie in Javascript auch in C++ ermöglicht, jede beliebige Information in einer entsprechenden Variablen zu speichern.

#include <boost/any.hpp> 

int main() 
{ 
  boost::any a = 1; 
  a = 3.14; 
  a = true; 
} 

Um boost::any verwenden zu können, muss die Headerdatei boost/any.hpp eingebunden werden. Sie können dann Objekte vom Typ boost::any erstellen, in denen Sie jede beliebige Art von Information speichern können.

Beachten Sie, dass in einer Variablen vom Typ boost::any nicht wirklich jede beliebige Art von Information gespeichert werden kann. Boost.Any verlangt bestimmte Voraussetzungen, wenn auch minimale. So muss der Typ der Information, die in einer boost::any-Variablen gespeichert werden soll, copy-constructible sein. Das bedeutet zum Beispiel, dass eine Zeichenkette nicht direkt in einer boost::any-Variablen gespeichert werden kann, sondern so wie im folgenden Beispiel explizit auf std::string zugegriffen werden muss.

#include <boost/any.hpp> 
#include <string> 

int main() 
{ 
  boost::any a = 1; 
  a = 3.14; 
  a = true; 
  a = std::string("Hello, world!"); 
} 

Würde im obigen Programm versucht werden, die Zeichenkette "Hello, world!" direkt a zuzuweisen, würde der Compiler mit einem Fehler abbrechen. Der Grund ist, dass der Datentyp einer Zeichenkette wie "Hello, world!" ein Array bestehend aus char-Elementen ist und Arrays in C++ nicht copy-constructible sind.

Um auf den Inhalt einer boost::any-Variablen zuzugreifen, muss der Cast-Operator boost::any_cast verwendet werden.

#include <boost/any.hpp> 
#include <iostream> 

int main() 
{ 
  boost::any a = 1; 
  std::cout << boost::any_cast<int>(a) << std::endl; 
  a = 3.14; 
  std::cout << boost::any_cast<double>(a) << std::endl; 
  a = true; 
  std::cout << boost::any_cast<bool>(a) << std::endl; 
} 

Indem der entsprechende Datentyp als Template-Parameter an boost::any_cast übergeben wird, wird der Inhalt der boost::any-Variablen konvertiert. Sollte ein ungültiger Datentyp angegeben werden und eine entsprechende Konvertierung nicht durchgeführt werden können, wird eine Ausnahme vom Typ boost::bad_any_cast geworfen.

#include <boost/any.hpp> 
#include <iostream> 

int main() 
{ 
  try 
  { 
    boost::any a = 1; 
    std::cout << boost::any_cast<float>(a) << std::endl; 
  } 
  catch (boost::bad_any_cast &e) 
  { 
    std::cerr << e.what() << std::endl; 
  } 
} 

Im obigen Beispielprogramm wird eine Ausnahme geworfen, weil der als Template-Parameter übergebene Datentyp float nicht dem Datentypen int entspricht, der in der Variablen a verwendet wird. Es ist wichtig, jeweils genau den Datentyp als Template-Parameter anzugeben, der in der entsprechenden boost::any-Variablen verwendet wird. So würde obiges Programm auch dann eine Ausnahme werfen, wenn als Template-Parameter short oder long angegeben werden würde.

Da die Klasse boost::bad_any_cast von std::bad_cast abgeleitet ist, können Sie in catch-Handlern auch Ausnahmen dieses Datentyps abfangen.

Möchten Sie wissen, ob in einer Variablen vom Typ boost::any ein Wert gespeichert ist, können Sie die Methode empty() aufrufen. Um zu erfahren, welchen Datentyp ein in boost::any gespeicherter Wert besitzt, können Sie type() verwenden.

#include <boost/any.hpp> 
#include <typeinfo> 
#include <iostream> 

int main() 
{ 
  boost::any a = 1; 
  if (!a.empty()) 
  { 
    const std::type_info &ti = a.type(); 
    std::cout << ti.name() << std::endl; 
  } 
} 

Im obigen Beispielprogramm werden beide Methoden empty() und type() verwendet. Während empty() einen Wahrheitswert zurückgibt, ist der Typ des Rückgabewerts von type() die in der Headerdatei typeinfo definierte Klasse std::type_info.

Abschließend sehen Sie, wie Sie mit boost::any_cast einen Zeiger auf einen Wert in einer boost::any-Variable erhalten.

#include <boost/any.hpp> 
#include <iostream> 

int main() 
{ 
  boost::any a = 1; 
  int *i = boost::any_cast<int>(&a); 
  std::cout << *i << std::endl; 
} 

Sie müssen lediglich einen Zeiger auf die boost::any-Variable als Parameter an boost::any_cast übergeben. Der Template-Parameter bleibt unverändert.


14.4 Boost.Variant

Der Unterschied zwischen Boost.Variant und Boost.Any ist, dass die von Boost.Variant zur Verfügung gestellte Datenstruktur Werte von einer festgelegten Anzahl an Typen speichern kann, während in einem boost::any ein Wert jedes beliebigen Datentyps gespeichert werden kann. Sehen Sie sich dazu folgendes Beispiel an.

#include <boost/variant.hpp> 

int main() 
{ 
  boost::variant<double, char> v; 
  v = 3.14; 
  v = 'A'; 
} 

Die Bibliothek Boost.Variant stellt eine Klasse boost::variant zur Verfügung, die in der Headerdatei boost/variant.hpp definiert ist. Da es sich bei boost::variant um ein Template handelt, muss mindestens ein Template-Parameter angegeben werden. Der oder die Template-Parameter beschreiben, welche Datentypen unterstützt und gespeichert werden können. Für obiges Programm bedeutet dies, dass in der Variablen v entweder ein double- oder ein char-Wert gespeichert werden kann. Würden Sie versuchen, der Variablen v eine Zahl vom Typ int zuzuweisen, würde der Code nicht kompilieren.

Während sich obiges Programm auch mit einer union-Struktur entwickeln lässt, können in der Klasse boost::variant auch Datentypen wie zum Beispiel std::string untergebracht werden - etwas, was mit union-Strukturen nicht funktioniert, da diese keine Klassen enthalten dürfen.

#include <boost/variant.hpp> 
#include <string> 

int main() 
{ 
  boost::variant<double, char, std::string> v; 
  v = 3.14; 
  v = 'A'; 
  v = "Hello, world!"; 
} 

Um die in der Variablen v gespeicherten Werte auszugeben, greifen Sie auf eine freistehende Funktion boost::get() zu.

#include <boost/variant.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  boost::variant<double, char, std::string> v; 
  v = 3.14; 
  std::cout << boost::get<double>(v) << std::endl; 
  v = 'A'; 
  std::cout << boost::get<char>(v) << std::endl; 
  v = "Hello, world!"; 
  std::cout << boost::get<std::string>(v) << std::endl; 
} 

boost::get() erwartet als Template-Parameter einen der Datentypen, die für die entsprechende Variable erlaubt sind. Geben Sie einen ungültigen Datentyp an, führt dies zu einem Laufzeitfehler. Es findet also keine Überprüfung der Datentypen zur Kompilierung statt.

Wenn Sie lediglich Werte auf einen Stream wie die Standardausgabe ausgeben möchten, können Sie die Gefahr insofern umgehen als dass Sie Variablen vom Typ boost::variant direkt in den Stream hineinschreiben können.

#include <boost/variant.hpp> 
#include <string> 
#include <iostream> 

int main() 
{ 
  boost::variant<double, char, std::string> v; 
  v = 3.14; 
  std::cout << v << std::endl; 
  v = 'A'; 
  std::cout << v << std::endl; 
  v = "Hello, world!"; 
  std::cout << v << std::endl; 
} 

Möchten Sie Werte nicht auf einen Stream ausgeben, sondern anderweitig verarbeiten, bietet Boost.Variant Ihnen eine Funktion boost::apply_visitor() an.

#include <boost/variant.hpp> 
#include <boost/any.hpp> 
#include <vector> 
#include <string> 
#include <iostream> 

std::vector<boost::any> vector; 

struct output : 
  public boost::static_visitor<> 
{ 
  void operator()(double &d) const 
  { 
    vector.push_back(d); 
  } 

  void operator()(char &c) const 
  { 
    vector.push_back(c); 
  } 

  void operator()(std::string &s) const 
  { 
    vector.push_back(s); 
  } 
}; 

int main() 
{ 
  boost::variant<double, char, std::string> v; 
  v = 3.14; 
  boost::apply_visitor(output(), v); 
  v = 'A'; 
  boost::apply_visitor(output(), v); 
  v = "Hello, world!"; 
  boost::apply_visitor(output(), v); 
} 

Der Funktion boost::apply_visitor() wird als erster Parameter ein Objekt vom Typ einer Klasse übergeben, die von boost::static_visitor abgeleitet sein muss. Die Klasse muss für jeden Datentyp der boost::variant-Variable, für die sie verwendet werden wird, den Operator operator()() überladen. Im obigen Beispiel muss dieser Operator entsprechend dreimal überladen sein, weil die Variable v die Datentypen double, char und std::string unterstützt.

Wenn Sie sich den Code oben genau ansehen, stellen Sie fest, dass boost::static_visitor ein Template ist. Der Datentyp des Rückgabewertes der überladenen Operatoren operator()() muss als Template-Parameter übergeben werden. Besitzen wie im obigen Programm die Methoden keinen Rückgabewert, wird kein Template-Parameter angegeben.

Der zweite Parameter, der an boost::apply_visitor() übergeben wird, ist eine Variable vom Typ boost::variant.

Wird boost::apply_visitor() verwendet, wird automatisch der überladene Operator operator()() für den ersten Parameter aufgerufen, der zum momentan verwendeten Datentypen im zweiten Parameter passt. Das bedeutet für obiges Beispiel, dass bei jedem Aufruf von boost::apply_visitor() ein anderer überladener Operator aufgerufen wird - zuerst der für double, dann der für char und abschließend der für std::string.

Der Vorteil von boost::apply_visitor() ist, dass nicht nur automatisch der richtige Operator aufgerufen wird und der entsprechende Wert aus der boost::variant-Variablen als Parameter übergeben wird. boost::apply_visitor() stellt außerdem sicher, dass für alle von einer boost::variant-Variablen unterstützen Datentypen tatsächlich Operatoren überladen wurden. Wäre im obigen Beispiel eine der drei Methoden vergessen worden, würde der Code nicht kompilieren.

Wenn wie im obigen Beispiel mehrere überladene Operatoren das gleiche tun, kann der Code mit Hilfe eines Templates vereinfacht werden.

#include <boost/variant.hpp> 
#include <boost/any.hpp> 
#include <vector> 
#include <string> 
#include <iostream> 

std::vector<boost::any> vector; 

struct output : 
  public boost::static_visitor<> 
{ 
  template <typename T> 
  void operator()(T &t) const 
  { 
    vector.push_back(t); 
  } 
}; 

int main() 
{ 
  boost::variant<double, char, std::string> v; 
  v = 3.14; 
  boost::apply_visitor(output(), v); 
  v = 'A'; 
  boost::apply_visitor(output(), v); 
  v = "Hello, world!"; 
  boost::apply_visitor(output(), v); 
} 

Da bei der Funktion boost::apply_visitor() zur Kompilierung sichergestellt wird, dass der Code korrekt ist, sollte diese Funktion grundsätzlich boost::get() vorgezogen werden.


14.5 Aufgaben

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Definieren Sie einen Datentyp configuration, der Name-Wert-Paare speichern kann. Namen sind dabei vom Typ std::string, Werte vom Typ std::string, int oder float. Speichern Sie dann in der Funktion main() folgende Name-Wert-Paare in einem Objekt vom Typ configuration: path=C:\Windows, version=3 und pi=3.1415. Geben Sie dann die Name-Wert-Paare zum Test auf die Standardausgabe aus.

  2. Erweitern Sie Ihre Lösung zu Aufgabe 1 dahingehend, dass nach der Ausgabe der Name-Wert-Paare path auf den neuen Wert C:\Windows\System gesetzt wird. Geben Sie anschließend zum Test den neuen Wert von path auf die Standardausgabe aus.